#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include "parse_options.h"

#define OPT_SHORT   1
static int opterror(const struct option *opt, const char *reason, int flags)
{
	if (flags & OPT_SHORT)
		fprintf(stderr, "option '%c' %s\n", opt->short_name, reason);
    else
	    fprintf(stderr, "option '%s' %s\n", opt->long_name, reason);
    return -1;
}

static int parse_options_usage(struct parse_opt_ctx_t *ctx,
			                   const char * const *usagestr,
			                   const struct option *opts)
{
	if (!usagestr)
		return parse_opt_help;

	fprintf(stderr, "usage: %s\n", *usagestr++);
	while (*usagestr && **usagestr)
		fprintf(stderr, "    or: %s\n", *usagestr++);

	while (*usagestr) {

		if (**usagestr)
			fprintf(stderr, "    %s\n", *usagestr);
		else
			putchar('\n');
		usagestr++;
	}

	fprintf(stderr, "options:\n");
	for (; opts->type != opt_t_end; opts++) {
		size_t pos;
		int pad;

		pos = fprintf(stderr, "    ");
		if (opts->short_name)
			pos += fprintf(stderr, "-%c", opts->short_name);
		if (opts->long_name && opts->short_name)
			pos += fprintf(stderr, ", ");
		if (opts->long_name)
			pos += fprintf(stderr, "--%s", opts->long_name);
		if (pos <= 24)	/* usage_opts_width */
			pad = 24 - pos;
		else {
			fputc('\n', stderr);
			pad = 24;
		}
		fprintf(stderr, "%*s%s\n", pad+2, "", opts->help);
	}

	fputc('\n', stderr);
	return parse_opt_help;
}

static void check_typos(const char *arg, const struct option *options)
{
	if (strlen(arg) < 3)
		return;

	fprintf(stderr, "did you mean `--%s` (with two dashes ?)", arg);

	for (; options->type != opt_t_end; options++) {
		if (!options->long_name)
			continue;
		if (!strncmp(options->long_name, arg, 3)) {
			fprintf(stderr, "or `--%s\n`", options->long_name);
		}
	}
}


static int get_arg(struct parse_opt_ctx_t *p, const struct option *opt,
		   int flags, const char **arg)
{
	if (p->argc > 1) {
		p->argc--;
		*arg = *++p->argv;
	} else {
		return opterror(opt, "requires a value", flags);
	}
	return 0;
}

static int get_value(struct parse_opt_ctx_t *p,
		     const struct option *opt, int flags)
{
    const char *s, *arg;

    if ((flags & OPT_SHORT) && p->opt && (opt->flags & opt_f_noarg))
        return opterror(opt, "takes no value", flags);

	switch (opt->type) {
	case opt_t_string:
		if (opt->flags & opt_f_noarg && !p->opt)
			*(const char **)opt->value = (const char *)opt->defval;
		else
			return get_arg(p, opt, flags, (const char **)opt->value);
		return 0;

	case opt_t_int:
		if (opt->flags & opt_f_noarg && !p->opt) {
			*(int *)opt->value = (int)opt->defval;
			return 0;
		}
		if (get_arg(p, opt, flags, &arg))
			return -1;
		*(int *)opt->value = strtol(arg, (char **)&s, 10);
		if (*s)
			return opterror(opt, "expects a numerical value", flags);
		return 0;

	case opt_t_bool:
        *(int *)opt->value = 1;
		return 0;
	default:
		break;
	}
	return -1;
}

static int parse_short_opt(struct parse_opt_ctx_t *p,
			               const struct option *options)
{
	for (; options->type != opt_t_end; options++) {
		if (options->short_name == *p->opt) {
			p->opt = p->opt[1] ? p->opt + 1 : NULL; /* next opt */
			return get_value(p, options, OPT_SHORT);
		}
		continue;
	}
    return -2;
}

static int parse_long_opt(struct parse_opt_ctx_t *p,
				          const struct option *options)
{
	for (; options->type != opt_t_end; options++) {

		int opt_flags = 0;
		if (!options->long_name)
			continue;

        if (!strcmp(options->long_name, p->opt)) {
            p->opt = NULL;  /* next opt always NULL */
		    return get_value(p, options, opt_flags);
        }
        continue;
	}
	return -2;
}

void parse_options_prepare(struct parse_opt_ctx_t *ctx,
                           int argc, const char **argv,
                           const struct option *options, enum parse_f flags)
{
    memset(ctx, 0, sizeof(*ctx));
    ctx->argc = argc - 1;
    ctx->argv = argv + 1;
    ctx->out  = argv;
    ctx->cpidx = 0;
    ctx->flags = flags;
}

int parse_options_step(struct parse_opt_ctx_t *ctx,
                       const struct option *options,
                       const char * const usagestr[])
{
    ctx->opt = NULL;
    for (; ctx->argc; ctx->argc--, ctx->argv++) {
        const char *arg = ctx->argv[0];

        if (*arg != '-' || !arg[1]) { /* not a option */
			if(ctx->flags & parse_f_stop_at_non_option)
				return parse_opt_non_option;

			ctx->out[ctx->cpidx++] = ctx->argv[0];
			continue;
		}

       	if (arg[1] != '-') { /* '-' */
			ctx->opt = arg + 1; /* skip '-' */

            do {
                switch (parse_short_opt(ctx, options)) {
                case -1:
                    return parse_options_usage(ctx, usagestr, options);
                case -2:
                    if (ctx->opt)
                        check_typos(arg + 1, options);
		            return parse_opt_unknow;
                }
            } while(ctx->opt);

			continue;
		}

		if (!arg[2]) { /* "--" */
			ctx->argc--;
			ctx->argv++;
			break;
		}

        ctx->opt = arg + 2;
		switch (parse_long_opt(ctx, options)) {
		case -1:
			return parse_options_usage(ctx, usagestr, options);
        case -2:
            if (ctx->opt)
                check_typos(arg + 2, options);
		    return parse_opt_unknow;
		}
		continue;
    }
    return parse_opt_done;
}

int parse_options_finalize(struct parse_opt_ctx_t *ctx)
{
    memmove( ctx->out+ctx->cpidx, ctx->argv,  ctx->argc * sizeof(*ctx->out));
	ctx->out[ctx->cpidx + ctx->argc] = NULL;
	return ctx->cpidx + ctx->argc;
}

int parse_options(int argc, char **argv, const struct option *options,
                  const char * const usagestr[], enum parse_f flags)
{
    struct parse_opt_ctx_t ctx;
	
	if( flags & parse_f_need_option && argc == 1) {
		parse_options_usage(&ctx, usagestr, options);
		return -1;
	}
	
    parse_options_prepare(&ctx, argc, (const char **)argv, options, flags);
    switch(parse_options_step(&ctx, options, usagestr)){
        case parse_opt_help:
            return -1;
        case parse_opt_done:
        case parse_opt_non_option:
            break;
        default: /* parse_opt_unknown */
            if (ctx.argv[0][1] == '-') { /* long_name */
                fprintf(stderr, "unknown option '%s'\n", ctx.argv[0] + 1);
            } else if (isascii(*ctx.opt)) {
                fprintf(stderr, "unknown switch '%c'\n", *ctx.opt);
            } else {
                fprintf(stderr, "unknown non-ascii option in string '%s'\n",
                        ctx.argv[0]);
            }
            parse_options_usage(&ctx, usagestr, options);
            break;
    }
    return parse_options_finalize(&ctx);
}